package com.budingge.common.ftputils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;

import com.budingge.common.ftputils.Status.DownloadStatus;
import com.budingge.common.ftputils.Status.FileType;
import com.budingge.common.ftputils.Status.UploadStatus;

/**
 * FTP 工具类
 * 
 * @author budingge.com
 */
public class FtpUtils {
	private static final Log log = LogFactory.getLog(FtpUtils.class);
	private static final String ENCODING_DEFAULT = "UTF8";
	private static final String ENCODING_ISO88591 = "ISO-8859-1";
	private FTPClient ftpClient;
	
	

	public FtpUtils(FTPClient ftpClient) {
		super();
		this.ftpClient = ftpClient;
	}

	public FTPClient getFtpClient() {
		return ftpClient;
	}

	/**
	 * 从FTP服务器上下载文件,支持断点续传，上传百分比汇报
	 * 
	 * @param remote
	 *            远程文件路径
	 * @param local
	 *            本地文件路径
	 * @return 上传的状态
	 * @throws IOException
	 */
	public DownloadStatus download(String remote, String local)
			throws IOException {
		// 设置被动模式
		ftpClient.enterLocalPassiveMode();
		// 设置以二进制方式传输
		ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
		ftpClient.setFileTransferMode(FTP.BINARY_FILE_TYPE);
		DownloadStatus result;
		// 检查远程文件是否存在
		FTPFile[] files = ftpClient.listFiles(new String(remote
				.getBytes(ENCODING_DEFAULT), ENCODING_ISO88591));
		if (files.length != 1) {
			log.info("远程文件 不存在 [" + remote + "]");
			return DownloadStatus.Remote_File_Noexist;
		}

		long lRemoteSize = files[0].getSize();
		File f = new File(local);
		// 本地存在文件，进行断点下载
		if (f.exists()) {
			long localSize = f.length();
			// 判断本地文件大小是否大于远程文件大小
			if (localSize >= lRemoteSize) {
				log.info("本地文件大于或者等于远程文件，中止下载");
				return DownloadStatus.Local_Bigger_Remote;
			}

			// 进行断点续传，并记录状态
			FileOutputStream out = new FileOutputStream(f, true);
			ftpClient.setRestartOffset(localSize);
			InputStream in = ftpClient.retrieveFileStream(new String(remote
					.getBytes(ENCODING_DEFAULT), ENCODING_ISO88591));
			byte[] bytes = new byte[1024];
			long step = lRemoteSize / 100;
			if(step == 0){
				step = 1;
			}
			long process = localSize / step;
			int c;
			while ((c = in.read(bytes)) != -1) {
				out.write(bytes, 0, c);

				localSize += c;
				long nowProcess = localSize / step;
				if (nowProcess > process) {
					process = nowProcess;
					if (process % 10 == 0)
						log.debug("文件 [" + remote + "] 的下载进度：" + process);
					// TODO 更新文件下载进度,值存放在process变量中
				}
			}
			in.close();
			out.close();
			boolean isDo = ftpClient.completePendingCommand();
			if (isDo) {
				result = DownloadStatus.Download_From_Break_Success;
			} else {
				result = DownloadStatus.Download_From_Break_Failed;
			}
		} else {
			OutputStream out = new FileOutputStream(f);
			InputStream in = ftpClient.retrieveFileStream(new String(remote
					.getBytes(ENCODING_DEFAULT), ENCODING_ISO88591));
			byte[] bytes = new byte[1024];
			long step = lRemoteSize / 100;
			if(step == 0){
				step = 1;
			}
			long process = 0;
			long localSize = 0L;
			int c;
			while ((c = in.read(bytes)) != -1) {
				out.write(bytes, 0, c);
				localSize += c;
				long nowProcess = localSize / step;
				if (nowProcess > process) {
					process = nowProcess;
					if (process % 10 == 0)
						log.debug("下载进度：" + process);
					// TODO 更新文件下载进度,值存放在process变量中
				}
			}
			in.close();
			out.close();
			boolean upNewStatus = ftpClient.completePendingCommand();
			if (upNewStatus) {
				result = DownloadStatus.Download_New_Success;
			} else {
				result = DownloadStatus.Download_New_Failed;
			}
		}
		return result;
	}

	/**
	 * 下载文件
	 * 
	 * @param remoteFileName
	 *            FTP服务器上的文件路径
	 * @param localFileName
	 *            文件下载后的本地存放路径
	 * @param ftpConfig
	 *            ftp配置(供下载发生异常时，记录日志用)
	 * @return 下载成功返回 true， 否则返回 false
	 * @throws IOException
	 */
	public boolean download(String remoteFileName, String localFileName,
			String ftpConfig) throws IOException {
		// 设置被动模式
		ftpClient.enterLocalPassiveMode();
		// 设置以二进制方式传输
		ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
		ftpClient.setFileTransferMode(FTP.BINARY_FILE_TYPE);
		boolean flag = false;
		OutputStream oStream = null;
		File outfile = new File(localFileName);
		File outDirectory = outfile.getParentFile();
		if (outDirectory != null && !outDirectory.exists()) {
			outDirectory.mkdirs();
		}
		try {
			oStream = new FileOutputStream(outfile);
			flag = ftpClient.retrieveFile(remoteFileName, oStream);
		} catch (IOException e) {
			flag = false;
			log.debug(e.getMessage(), e);
		} finally {
			if (oStream != null) {
				try {
					oStream.close();
				} catch (IOException e) {
					log.warn(e.getMessage());
				}
			}
		}
		return flag;
	}

	/**
	 * 上传文件到FTP服务器，支持断点续传
	 * 
	 * @param local
	 *            本地文件名称，绝对路径
	 * @param remote
	 *            远程文件路径，使用/home/directory1/subdirectory/file.ext或是
	 *            http://www.guihua.org/subdirectory/file.ext
	 *            按照Linux上的路径指定方式，支持多级目录嵌套，支持递归创建不存在的目录结构
	 * @return 上传结果
	 * @throws IOException
	 */
	public UploadStatus upload(String local, String remote) throws IOException {
		// 设置PassiveMode传输
		ftpClient.enterLocalPassiveMode();
		// 设置以二进制流的方式传输
		ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
		ftpClient.setFileTransferMode(FTP.BINARY_FILE_TYPE);
		UploadStatus result;
		// 对远程目录的处理
		String remoteFileName = remote;
		if (remote.contains("/")) {
			remoteFileName = remote.substring(remote.lastIndexOf("/") + 1);
			// 创建服务器远程目录结构，创建失败直接返回
			if (!createDirectory(remote)) {
				return UploadStatus.Create_Directory_Fail;
			}
		}

		// 检查远程是否存在文件
		FTPFile[] files = ftpClient.listFiles(new String(remoteFileName
				.getBytes(ENCODING_DEFAULT), ENCODING_ISO88591));
		if (files.length == 1) {
			long remoteSize = files[0].getSize();
			File f = new File(local);
			long localSize = f.length();
			if (remoteSize == localSize) {
				return UploadStatus.File_Exits;
			} else if (remoteSize > localSize) {
				return UploadStatus.Remote_Bigger_Local;
			}

			// 尝试移动文件内读取指针,实现断点续传
			result = uploadFile(remoteFileName, f, ftpClient, remoteSize);

			// 如果断点续传没有成功，则删除服务器上文件，重新上传
			if (result == UploadStatus.Upload_From_Break_Failed) {
				if (!ftpClient.deleteFile(remoteFileName)) {
					return UploadStatus.Delete_Remote_Faild;
				}
				result = uploadFile(remoteFileName, f, ftpClient, 0);
			}
		} else {
			result = uploadFile(remoteFileName, new File(local), ftpClient, 0);
		}
		return result;
	}

	/**
	 * 上传文件到服务器,新上传和断点续传
	 * 
	 * @param remoteFile
	 *            远程文件名，在上传之前已经将服务器工作目录做了改变
	 * @param localFile
	 *            本地文件File句柄，绝对路径
	 * @param ftpClient
	 *            FTPClient引用
	 * @param remoteSize
	 *            断点续传时上传文件的开始点
	 * @return
	 * @throws IOException
	 */
	public UploadStatus uploadFile(String remoteFile, File localFile,
			FTPClient ftpClient, long remoteSize) throws IOException {
		UploadStatus status;
		// 显示进度的上传
		long step = localFile.length() / 100;
		long process = 0;
		long localreadbytes = 0L;
		RandomAccessFile raf = new RandomAccessFile(localFile, "r");
		OutputStream out = ftpClient.appendFileStream(new String(remoteFile
				.getBytes(ENCODING_DEFAULT), ENCODING_ISO88591));
		// 断点续传
		if (remoteSize > 0) {
			ftpClient.setRestartOffset(remoteSize);
			process = remoteSize / step;
			raf.seek(remoteSize);
			localreadbytes = remoteSize;
		}
		byte[] bytes = new byte[1024];
		int c;
		while ((c = raf.read(bytes)) != -1) {
			out.write(bytes, 0, c);
			localreadbytes += c;
			if (localreadbytes / step != process) {
				process = localreadbytes / step;
				System.out.println("上传进度:" + process);
				// TODO 汇报上传状态
			}
		}
		out.flush();
		raf.close();
		out.close();
		boolean result = ftpClient.completePendingCommand();
		if (remoteSize > 0) {
			status = result ? UploadStatus.Upload_From_Break_Success
					: UploadStatus.Upload_From_Break_Failed;
		} else {
			status = result ? UploadStatus.Upload_New_File_Success
					: UploadStatus.Upload_New_File_Failed;
		}
		return status;
	}

	/**
	 * 上传文件
	 * 
	 * @param fileName
	 * @param newName
	 * @return
	 * @throws IOException
	 */
	public boolean uploadFile(String fileName, String newName)
			throws IOException {
		// 设置PassiveMode传输
		ftpClient.enterLocalPassiveMode();
		// 设置以二进制流的方式传输
		ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
		ftpClient.setFileTransferMode(FTP.BINARY_FILE_TYPE);
		boolean flag = false;
		InputStream iStream = null;
		try {
			iStream = new FileInputStream(fileName);
			flag = ftpClient.storeFile(newName, iStream);
		} catch (IOException e) {
			log.debug(e.getMessage(), e);
			flag = false;
			return flag;
		} finally {
			if (iStream != null) {
				iStream.close();
			}
		}
		return flag;
	}

	/**
	 * 上传文件
	 * 
	 * @param fileName
	 * @return
	 * @throws IOException
	 */
	public boolean uploadFile(String fileName) throws IOException {
		return uploadFile(fileName, fileName);
	}

	/**
	 * 上传文件
	 * 
	 * @param iStream
	 * @param newName
	 * @return
	 * @throws IOException
	 */
	public boolean uploadFile(InputStream iStream, String newName)
			throws IOException {
		// 设置PassiveMode传输
		ftpClient.enterLocalPassiveMode();
		// 设置以二进制流的方式传输
		ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
		ftpClient.setFileTransferMode(FTP.BINARY_FILE_TYPE);
		boolean flag = false;
		try {
			// can execute [OutputStream storeFileStream(String remote)]
			// Above method return's value is the local file stream.
			flag = ftpClient.storeFile(newName, iStream);
		} catch (IOException e) {
			flag = false;
			return flag;
		} finally {
			if (iStream != null) {
				iStream.close();
			}
		}
		return flag;
	}

	/**
	 * 递归创建远程 FTP 服务器目录
	 * 
	 * @param directory
	 * @return
	 * @throws IOException
	 */
	public boolean createDirectory(String directory) throws IOException {
		String workingDirectory = ftpClient.printWorkingDirectory(); // 当前的工作目录
		boolean flag = true;
		flag = changeWorkingDirectory(directory);// 判断目录是否存在
		if (!flag) {
			flag = ftpClient.makeDirectory(directory);
		} else {
			log.debug("目录 [" + ftpClient.printWorkingDirectory() + "] 已在存在");
		}
		ftpClient.changeWorkingDirectory(workingDirectory);
		return flag;
	}

	/**
	 * 切换工作目录
	 * 
	 * @param remote_path
	 * @return
	 * @throws IOException
	 */
	public boolean changeWorkingDirectory(String remote_path)
			throws IOException {
		return ftpClient.changeWorkingDirectory(remote_path);
	}

	/**
	 * 删除目录
	 * 
	 * @param path
	 * @return
	 * @throws IOException
	 */
	public boolean removeDirectory(String path) throws IOException {
		return ftpClient.removeDirectory(path);
	}

	/**
	 * 删除目录(及目录下所有文件和子目录)
	 * 
	 * @param path
	 * @param isAll
	 * @return
	 * @throws IOException
	 */
	public boolean removeDirectory(String path, boolean isAll)
			throws IOException {
		if (!isAll) {
			return removeDirectory(path);
		}

		FTPFile[] ftpFileArr = ftpClient.listFiles(path);
		if (ftpFileArr == null || ftpFileArr.length == 0) {
			return removeDirectory(path);
		}

		for (FTPFile ftpFile : ftpFileArr) {
			String name = ftpFile.getName();
			if (".".equals(name) || "..".equals(name)) {
				continue;
			}
			if (ftpFile.isDirectory()) {
				log.info("Delete subPath [" + path + "/" + name + "]");
				removeDirectory(path + "/" + name, true);
			} else if (ftpFile.isFile()) {
				log.info("Delete file [" + path + "/" + name + "]");
				deleteFile(path + "/" + name);
			} else if (ftpFile.isSymbolicLink()) {

			} else if (ftpFile.isUnknown()) {

			}
		}
		return ftpClient.removeDirectory(path);
	}

	/**
	 * 检查目录是否存在
	 * 
	 * @param path
	 * @return
	 * @throws IOException
	 */
	public boolean existDirectory(String path) throws IOException {
		// 查看是否能够切换到指定目录
		return ftpClient.changeWorkingDirectory(FilenameUtils
				.separatorsToUnix(path));
	}

	/**
	 * 检查文件是否存在
	 * 
	 * @param path
	 * @return
	 * @throws IOException
	 */
	public boolean existFile(String remote_file_path) throws IOException {
		return getFileinfo(remote_file_path) != null;
	}

	/**
	 * 获取文件
	 * 
	 * @param remote_file_path
	 * @return
	 * @throws Exception
	 */
	public FTPFile getFileinfo(String remote_file_path) throws IOException {
		FTPFile ftpFile = null;
		FTPFile[] ftpFileArr = ftpClient.listFiles(FilenameUtils
				.separatorsToUnix(remote_file_path));
		if (ftpFileArr.length == 1) {
			ftpFile = ftpFileArr[0].isFile() ? ftpFileArr[0] : null;
		}
		return ftpFile;
	}

	/**
	 * 获取文件
	 * 
	 * @param remote_file_path
	 * @return
	 * @throws IOException
	 */
	public long getFilesize(String remote_file_path) throws IOException {
		long remote_file_size = -1;
		FTPFile ftpFile = getFileinfo(remote_file_path);
		if (ftpFile != null) {
			remote_file_size = ftpFile.getSize();
		}
		return remote_file_size;
	}

	/**
	 * 判断ftp是否还连接着
	 * 
	 * @return
	 */
	public boolean isControlConnectionedClosed() {
		return ftpClient.getReplyCode() == 221;
	}

	/**
	 * 得到FTP指定目录下的所有文件名列表（不包括目录）
	 * 
	 * @param path
	 *            如果 path 为 null, 则列出当前所在目录的所有文件名
	 * @return
	 * @throws IOException
	 */
	public List<String> getFileList(String path) throws IOException {
		return getFileList(path, null);
	}

	/**
	 * 得到FTP指定目录下的符合命名规则的文件名列表（不包括目录）
	 * 
	 * @param path
	 *            如果 path 为 null, 则列出当前所在目录的所有文件名
	 * @param regex
	 *            文件名的命名规则，即正则表达式, 需要注意的是：此处用正则表达式匹配文件名， 不区分大小写。<br/>
	 *            如 ".*\.(txt|xml)", 则是只取后缀名为 txt 或 xml 的文件
	 * @return 如果此文件夹下没有文件，则返回一个长度为 0 的List
	 * @throws IOException
	 */
	public List<String> getFileList(String path, String regex)
			throws IOException {
		// listFiles return contains directory and file, it's FTPFile instance
		// listNames() contains directory,
		// so using following to filer directory.String[] fileNameArr =
		// ftpClient.listNames(path);

		FTPFile[] ftpFiles = StringUtils.isBlank(path) ? ftpClient.listFiles()
				: ftpClient.listFiles(path);
		Pattern pattern = StringUtils.isBlank(regex) ? null : Pattern.compile(
				regex, Pattern.CASE_INSENSITIVE);
		List<String> retList = new ArrayList<String>();
		if (ftpFiles == null || ftpFiles.length == 0) {
			return retList;
		}

		String fileName = null;
		for (FTPFile ftpFile : ftpFiles) {
			fileName = ftpFile.getName();
			if (ftpFile.isFile()
					&& (pattern == null || pattern.matcher(fileName).matches())) {
				retList.add(fileName);
			}
		}
		return retList;
	}

	/**
	 * 删除文件
	 * 
	 * @param pathName
	 * @return
	 * @throws IOException
	 */
	public boolean deleteFile(String pathName) throws IOException {
		// TODO 调用 existDirectory() 判断文件是否存在
		return ftpClient.deleteFile(pathName);
	}

	/**
	 * 重命名文件(夹)，说明：如果目标文件(夹)存在，则直接覆盖目标文件(夹)
	 * 
	 * @param from
	 *            原文件名
	 * @param to
	 *            新文件名
	 * @param fileType
	 *            文件类型 [{@link FileType#File}：文件；{@link FileType#Directory}：文件夹]
	 * @return 如果重命名成功，返回true, 否则返回false
	 * @throws IOException
	 * @throws UnsupportedEncodingException
	 */
	public boolean rename(String from, String to)
			throws UnsupportedEncodingException, IOException {
		boolean flag = createDirectory(FilenameUtils.getFullPath(to));
		if (flag) {
			flag = ftpClient.rename(FilenameUtils.separatorsToUnix(from),
					FilenameUtils.separatorsToUnix(to));
		}
		return flag;
	}

}